원본 항목: Part 2-2. 자격증명 관리 계열 — 8. 주석문안에 포함된 패스워드 등 시스템 주요정보CWE-615repo 폴더: sd27_commentleak/ (빌드·실행은 §3-1)
목표 시간: 1회차 15분 / 2회차 9분 / 3회차 6분
Part 2-2 마지막 드릴.
이번엔 Bad/Good이 “같은 기능의 두 구현”이 아니라 “배포 전 검사기가 있는가 없는가” 다 — 다른 각도의 치환 패턴을 익힌다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- int release_ok_bad(const char *source_code) { (void)source_code; return 1; } /* 검사 안 함 */+ int release_ok_good(const char *source_code) {+ return find_secret_in_comments(source_code) == 0; /* 주석을 스캔해서 위험 키워드 검사 */+ }
주석에 적어둔 패스워드·계정·키·접속 URL은 배포 전 반드시 제거한다 — 사람이 눈으로 확인하는 대신 자동 검사(grep, 정적분석) 로 강제한다.
회차별 진행
회차
변형
보는 것
순서
1회차
A 배포 파이프라인의 릴리스 게이트
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 한 줄 주석(//)도 검사 대상에 추가
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C 키워드 목록을 파일에서 로드
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
1. 요구사항서
1-A. 변형 A — 배포 파이프라인의 릴리스 게이트
소스 코드 문자열을 입력받아, 블록 주석(/* ... */) 안에 password, secret, apikey 같은 위험 키워드가 있으면 배포를 막는다.
대소문자를 구분하지 않는다.
함수
int find_secret_in_comments(const char *source_code);int release_ok_bad(const char *source_code);int release_ok_good(const char *source_code);
공격 입력표
소스 코드
Bad 결과
이유
주석에 default password is "abracadabra" 포함
배포 승인(1)
검사 자체를 안 한다
정상 주석만 있음
배포 승인(1)
위험 요소 없음
1-B. 변형 B — 한 줄 주석(//)도 검사 대상에 추가
C99/C++ 스타일 한 줄 주석까지 스캔하도록 find_secret_in_comments 를 확장
1-C. 변형 C — 키워드 목록을 파일에서 로드
하드코딩된 키워드 배열 대신, 팀이 관리하는 금지어 목록을 외부에서 주입받는 구조로 바꾼다(이 드릴 시리즈 전체를 관통하는 “화이트/블랙리스트는 외부화”의 마지막 반복)
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
변형별로 아래 pseudo code 를 소스 파일에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
다 쓰면 [ ] 를 x 로 바꾼다.
막히면 §5 모범답안(A)의 해당 단계만 본다.
2-A. release_ok_good — Level 1 (1회차, 전부 본다)
먼저 Bad 를 그대로 쳐서 §3 의 test_bad_is_vulnerable 이 통과하는지(= 실제로 뚫리는지) 확인한다.
그다음 Good 을 쓴다.
int release_ok_bad(const char *source_code){ /* 1. (void)source_code * 2. return 1 ← 검사 단계 자체가 없다 */}int release_ok_good(const char *source_code){ /* [ ] 1. find_secret_in_comments(source_code) 를 먼저 만든다 * "/*" 부터 "*/" 까지 블록 주석을 하나씩 찾는다 * 각 주석을 buf[512] 에 복사한다 (길이 상한, 널 종료) * KEYWORDS 중 하나라도 strcasestr 로 찾으면 → return 1 * 끝까지 없으면 → return 0 * KEYWORDS: password, passwd, secret, apikey, api_key */ /* [ ] 2. return find_secret_in_comments(source_code) == 0 */}
2-B. release_ok_good — Level 2 (2회차, 번호와 제목만 본다)
int release_ok_good(const char *source_code){ /* [ ] 1. find_secret_in_comments(source_code) 를 먼저 만든다 */ /* [ ] 2. return find_secret_in_comments(source_code) == 0 */}
2-C. release_ok_good — Level 3 (3회차, 백지)
int release_ok_good(const char *source_code){ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `strcasestr` 의 feature macro (`_DEFAULT_SOURCE`) * - 닫히지 않은 주석 (`*/` 가 없을 때) */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
#define _DEFAULT_SOURCE#include <string.h>#include <stddef.h>#include "release_check.h"static const char *KEYWORDS[] = { "password", "passwd", "secret", "apikey", "api_key" };#define KEYWORD_COUNT (sizeof(KEYWORDS) / sizeof(KEYWORDS[0]))int find_secret_in_comments(const char *source_code){ const char *p = source_code; while ((p = strstr(p, "/*")) != NULL) { const char *end = strstr(p, "*/"); size_t len = end ? (size_t)(end - p) : strlen(p); size_t i; char buf[512]; if (len >= sizeof(buf)) len = sizeof(buf) - 1; memcpy(buf, p, len); buf[len] = '\0'; for (i = 0; i < KEYWORD_COUNT; i++) if (strcasestr(buf, KEYWORDS[i]) != NULL) return 1; p = end ? end + 2 : p + 2; } return 0;}int release_ok_bad(const char *source_code) { (void)source_code; return 1; }int release_ok_good(const char *source_code) { return find_secret_in_comments(source_code) == 0; }
눈여겨볼 점: release_ok_bad 는 source_code 를 아예 읽지 않는다((void) 캐스팅으로 버림).
이 항목의 “취약점”은 잘못된 로직이 아니라 검사 단계 자체의 부재다 — SD09(외부 제어)에서 본 “단계 자체를 빠뜨리는” 패턴이 여기서도 반복된다.